/*
 * Copyright (c) 2007 jMouse Gestures
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMouse Gestures' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jmg.swing;

import com.jmg.gesture.GestureGroup;
import com.jmg.gesture.GestureManager;
import com.jmg.gesture.event.GestureListener;
import com.jmg.space.CoordinateFrame;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;


/**
 * SwingGestureManager class extends the abstract <code>GestureManager</code>
 * class to provide access between Swing Components and the core Gesture
 * classes.
 *
 * @author  Christopher Martin
 * @version $Id: SwingGestureManager.java 12 2007-05-24 11:54:07Z GentlemanHal $
 */
public class SwingGestureManager extends GestureManager<JComponent> implements
    MouseListener,
    MouseMotionListener
{
    /** Serial Version UID */
    private static final long serialVersionUID = 1L;
    
    private static final String ACTION_KEY_MAP_PRESSED  = "com.jmg.swing.KeyPressed";
    
    private static final String ACTION_KEY_MAP_RELEASED = "com.jmg.swing.KeyReleased";
    
    
    private JComponent registeredComponent;
    
    private boolean consumeEvents = true;
    
    private final Map<Integer, Object> mouseModifiers = new HashMap<Integer, Object>();
    
    private final Map<Integer, Object> keyModifiers = new HashMap<Integer, Object>();
    
    private final transient Map<Integer, Object> currentMouseModifiers = new HashMap<Integer, Object>();
    
    private final transient Map<Integer, Object> currentKeyModifiers = new HashMap<Integer, Object>();
    
    private final transient PointLocation previous = new PointLocation();
    
    
    
    
    
    /**
     * Creates a new instance of <code>SwingGestureManager</code>.
     *
     * @param name The name of this manager.
     */
    public SwingGestureManager(String name) {
        this(name, new ArrayList<GestureGroup>(), new ArrayList<GestureListener>());
    }
    
    /**
     * Creates a new instance of <code>SwingGestureManager</code>.
     * 
     * @param name The name of this manager.
     * @param gestures gestures The backing list collection type for mouse gestures.
     * @param listeners The backing list collection type for gesture listeners.
     */
    public SwingGestureManager(String name,
                               Collection<GestureGroup> gestures, 
                               Collection<GestureListener> listeners) 
    {
        super(name, CoordinateFrame.Top_Left_Front, gestures, listeners);
    }

    
    
    
    
    /**
     * Sets if mouse events should be consumed if a gesture is being performed.
     * The default is <code>true</code>.
     * 
     * @param b <code>true</code> to consume events, <code>false</code>
     *        otherwise.
     */
    public void setConsumeEvents(final boolean b) {
        consumeEvents = b;
    }
    
    /**
     * Returns if mouse events are being consumed while a gesture is being
     * performed.  The default is <code>true</code>.
     * 
     * @return <code>true</code> if events are being consumed,
     *         <code>false</code> otherwise.
     */
    public boolean isConsumeEvents() {
        return consumeEvents;
    }
    
    /**
     * Registers this manager on the given <code>Component</code>.  Once this
     * manager is registered on a <code>Component</code> it will start
     * listening for mouse and key events to check if they are valid to
     * start/stop a mouse gesture.
     * <p>
     * A manager can only be registered on one <code>Component</code> at a
     * time.
     * 
     * @param me The <code>Component</code> to register with.
     */
    @Override
    public void registerComponent(JComponent me) {
        if (null != me && registeredComponent != me) {
            
            if (null != registeredComponent) {
                unregisterComponent();
            }
                
            registeredComponent = me;
            
            for (Integer i : keyModifiers.keySet()) {
                addKeyActions(ACTION_KEY_MAP_PRESSED,  i);
                addKeyActions(ACTION_KEY_MAP_RELEASED, i);
            }
            
            me.addMouseListener(this);
            me.addMouseMotionListener(this);
        }
    }

    /**
     * Unregisters the registered <code>Component</code>.
     * <p>
     * If no <code>Component</code> is currently registered no action is taken
     * and no exception is thrown.
     */
    @Override
    public void unregisterComponent() {
        if (registeredComponent != null) {
            
            for (Integer i : keyModifiers.keySet()) {
                removeKeyActions(ACTION_KEY_MAP_PRESSED,  i);
                removeKeyActions(ACTION_KEY_MAP_RELEASED, i);
            }
            
            registeredComponent.removeMouseListener(this);
            registeredComponent.removeMouseMotionListener(this);

            registeredComponent = null;
        }
    }
    
    /**
     * Returns the registered <code>Component</code> or <code>null</code>
     * if no <code>Component</code> has been registered yet.
     * 
     * @return The <code>Component</code> or <code>null</code>.
     */
    @Override
    public JComponent getRegisteredComponent() {
        return registeredComponent;
    }
    
    /**
     * Returns the value of all the added mouse modifiers.
     * 
     * @return The mouse modifiers.
     */
    @Override
    public int getMouseModifiers() {
        int ret = 0;
        
        for (Integer i : mouseModifiers.keySet()) {
            ret += i;
        }
        
        return ret;
    }

    /**
     * Adds a mouse modifier to the current mouse modifiers.  The modifiers
     * used are the standard swing constants for the mouse buttons:
     * <pre>
     * {@link java.awt.event.MouseEvent#BUTTON1}
     * {@link java.awt.event.MouseEvent#BUTTON2}
     * {@link java.awt.event.MouseEvent#BUTTON3}
     * </pre>
     * 
     * @param mod The modifier to add.
     */
    @Override
    public void addMouseModifier(int mod) {
        mouseModifiers.put(mod, null);
    }

    /** 
     * Removes a mouse modifier from the current mouse modifiers.  If the
     * modifier was never added then nothing is removed.
     * 
     * @param mod The modifier to remove.
     */
    @Override
    public void removeMouseModifier(int mod) {
        mouseModifiers.remove(mod);
    }
    
    /**
     * Returns the value of all the added keyboard modifiers.
     * 
     * @return The keyboard modifiers.
     */
    @Override
    public int getKeyboardModifiers() {
        int ret = 0;
        
        for (Integer i : keyModifiers.keySet()) {
            ret += i;
        }
        
        return ret;
    }

    /**
     * Adds a mouse modifier to the current mouse modifiers.  The modifiers
     * used are the standard swing constants for the keys:
     * <pre>
     * {@link java.awt.event.KeyEvent#VK_A}
     * {@link java.awt.event.KeyEvent#VK_B}
     * etc
     * </pre>
     * 
     * @param mod The keyboard modifier to add.
     */
    @Override
    public void addKeyboardModifier(int mod) {
        keyModifiers.put(mod, null);
        
        if (registeredComponent != null) {
            addKeyActions(ACTION_KEY_MAP_PRESSED,  mod);
            addKeyActions(ACTION_KEY_MAP_RELEASED, mod);
        }
    }

    /**
     * Adds the modifier as a key binding to the registered component.
     * 
     * @param actionMapKey The key to add the binding under.
     * @param mod The key binding to add.
     */
    private void addKeyActions(String actionMapKey, int mod) {
        
        InputMap inputMap = registeredComponent.getInputMap(
                                    JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = registeredComponent.getActionMap();
        
        Object previousKey    = inputMap.get(KeyStroke.getKeyStroke(mod, 0));
        Action previousAction = actionMap.get(previousKey);
        
        if (previousAction != null) {
            actionMap.remove(previousKey);
        }
            
        inputMap.put(KeyStroke.getKeyStroke(mod, 0), actionMapKey);
        actionMap.put(actionMapKey,
                      new ModifierKeyAction(mod, true, previousAction, previousKey));
    }

    /**
     * Removes a keyboard modifier from the current key modifiers.  If the
     * modifier was never added then nothing is removed.
     * 
     * @param mod The keyboard modifier to remove.
     */
    @Override
    public void removeKeyboardModifier(int mod) {
        keyModifiers.remove(mod);
        
        if (registeredComponent != null) {
            removeKeyActions(ACTION_KEY_MAP_PRESSED,  mod);
            removeKeyActions(ACTION_KEY_MAP_RELEASED, mod);
            
            if (currentKeyModifiers.containsKey(mod)) {
                currentKeyModifiers.remove(mod);
            }
        }
        
    }

    /**
     * Removes the key bindings added to the registered components.
     * 
     * @param actionMapKey The key of the binding to remove.
     * @param mod The key binding to remove.
     */
    private void removeKeyActions(String actionMapKey, int mod) {        
        InputMap inputMap = registeredComponent.getInputMap(
                                    JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = registeredComponent.getActionMap();
        
        if (actionMap.get(actionMapKey) instanceof ModifierKeyAction) {
            ModifierKeyAction keyAction = (ModifierKeyAction) actionMap.get(actionMapKey);
            
            if (keyAction.getPreviousAction() == null) {
                inputMap.remove(KeyStroke.getKeyStroke(mod, 0));
                
            } else {
                restoreOriginalKeyBinding(keyAction, mod);
                
            }
            
            actionMap.remove(actionMapKey);
        }
        
    }

    /**
     * Restores any original key bindings that were removed when the key
     * modifier bindings were added.
     * 
     * @param keyAction The key modifier binding action that contains the
     *        original action key and action.
     * @param mod The key modifier binding.
     */
    private void restoreOriginalKeyBinding(ModifierKeyAction keyAction, int mod) {
        InputMap inputMap = registeredComponent.getInputMap(
                                    JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = registeredComponent.getActionMap();
        
        inputMap.put(KeyStroke.getKeyStroke(mod, 0), keyAction.getPreviousKey());
        actionMap.put(keyAction.getPreviousKey(), keyAction.getPreviousAction());
    }
    
    /**
     * Checks to see if the current mouse/keyboard modifiers match the
     * required mouse/keyboard modifiers needed to start a gesture.
     * 
     * @return <code>true</code> if the current modifiers are valid,
     *         <code>false</code> otherwise.
     */
    private boolean checkForValidMods() {
        if (currentMouseModifiers.isEmpty()) {
            return false;
        }
        
        
        int currentMouseMods = 0;
        int currentKeyMods   = 0;
        
        for (Integer i : currentMouseModifiers.keySet()) {
            currentMouseMods += i;
        }

        for (Integer i : currentKeyModifiers.keySet()) {
            currentKeyMods   += i;
        }
        
        return getMouseModifiers() == currentMouseMods 
               && (getKeyboardModifiers() == currentKeyMods || 0 == getKeyboardModifiers());
    }
    
    /**
     * Checks to see if events should be consumed, if they should then the
     * passed event is consumed.
     *
     * @param e The event to consume.
     */
    private void consume(MouseEvent e) {
        if (consumeEvents) {
            e.consume();
        }
    }

    
    
    
    
    /**
     * Adds the mouse button that was pressed to the current list of
     * modifiers and checks to see if they are valid for a mouse gesture to
     * begin.
     * <p>
     * <strong>This method should never be called manually.</strong>
     * </p>
     * 
     * @param e The mouse event used to find out which button was pressed.
     */
    public void mousePressed(MouseEvent e) {
        currentMouseModifiers.put(e.getButton(), null);

        if (!inGesture && checkForValidMods()) {
            previous.update(e.getPoint());

            startGesture(previous);

            consume(e);

        } else if (inGesture) {
            consume(e);
        }
    }

    /**
     * Removes the mouse button that was released from the current list of
     * modifiers and checks to see if a gesture is currently being performed.
     * If one is it checks to make sure the mouse modifiers are still valid.
     * If they are not longer valid the gesture is ended.
     * <p>
     * <strong>This method should never be called manually.</strong>
     * </p>
     * 
     * @param e The mouse event.
     */
    public void mouseReleased(MouseEvent e) {
        currentMouseModifiers.remove(e.getButton());

        if (inGesture && !checkForValidMods()) {
            previous.update(e.getPoint());
            
            endGesture(previous);

            consume(e);
            
        } else if (inGesture) {
            consume(e);
        }
    }
    
    /**
     * If a gesture is being performed it notifies all listeners.
     * <p>
     * <strong>This method should never be called manually.</strong>
     * </p>
     * 
     * @param e The mouse event.
     */
    public void mouseDragged(MouseEvent e) {
        if (inGesture) {
            long when = System.currentTimeMillis();
            previous.update(e.getPoint());
            
            for (GestureListener l : listeners) {
                l.gestureMovement(when, previous, this);
            }

            consume(e);
            
        } /*else {
            previous.update(e.getPoint());
        }*/
    }
    
    /**
     * Checks if a gesture is being performed and calls
     * {@link #consume} if one is.
     * <p>
     * <strong>This method should never be called manually.</strong>
     * </p>
     * 
     * @param e The mouse event.
     */
    public void mouseClicked(MouseEvent e) {
        if (inGesture) {
            consume(e);
        }
    }
    
    /** Not used by this manager. */
    public void mouseMoved(MouseEvent e)   { /* NOOP */ }
    /** Not used by this manager. */
    public void mouseEntered(MouseEvent e) { /* NOOP */ }
    /** Not used by this manager. */
    public void mouseExited(MouseEvent e)  { /* NOOP */ }

    
    
    

    /**
     * Returns a <code>String</code> representation of this instance.
     * 
     * @return The <code>String</code> representation.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        
        buf.append(getClass().getName()); 
        buf.append('[').append(super.toString());
        buf.append(",consumeEvents=").append(consumeEvents);
        buf.append(",mouseMods=").append(mouseModifiers.keySet());
        buf.append(",keyMods=").append(keyModifiers.keySet());
        buf.append(",registeredComponent=");
        
        if (null == registeredComponent) {
            buf.append("null"); 
        } else {
            buf.append(registeredComponent.getClass().getName());
            buf.append("[name=").append(registeredComponent.getName());
            buf.append(']');
        }
        
        buf.append(']');
        
        return buf.toString();
    }

    /**
     * Indicates whether some other object is "equal to" this one.
     * 
     * @param  obj The reference object with which to compare.
     * @return <code>true</code> if this object is the same as the obj
     *         argument; <code>false</code> otherwise.
     * @see    #hashCode()
     * @see    java.util.Hashtable
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        
        if (null == obj || obj.getClass() != getClass()) {
            return false;
        }
            
        SwingGestureManager sgm = (SwingGestureManager) obj;
            
        return super.equals(obj)                     &&
               sgm.consumeEvents == consumeEvents    &&
               (null == sgm.registeredComponent
                    ? null == registeredComponent
                    : sgm.registeredComponent.equals(registeredComponent)) &&
               sgm.keyModifiers.equals(keyModifiers) &&
               sgm.mouseModifiers.equals(mouseModifiers);
    }

    /**
     * Returns a hash code value for the object.
     * 
     * @return A hash code value for this object.
     * @see    java.lang.Object#equals(java.lang.Object)
     * @see    java.util.Hashtable
     */
    @Override
    public int hashCode() {
        return super.hashCode()                       +
               (consumeEvents ? 1231 : 1237)          +
               (registeredComponent == null 
                    ? 0
                    : registeredComponent.hashCode()) +
               keyModifiers.hashCode()                +
               mouseModifiers.hashCode();
    }
    
    
    
    
    
    /**
     * ModifierKeyAction class.
     *
     * @author Christopher Martin
     */
    private class ModifierKeyAction extends AbstractAction {
        /** Serial Version UID */
        private static final long serialVersionUID = 1L;
    
        private int key;
        private boolean pressed;
        private Action originalAction;
        private Object previousKey;
        
        
        
        /**
         * Creates a new instance of <code>ModifierKeyAction</code>.
         * 
         * @param key The constant value of the modifier key.
         * @param pressed <code>true</code> if the action to be performed is on
         *        key pressed, <code>false</code> if it is on key released.
         * @param originalAction The original <code>Action</code> that was
         *        bound to the previousKey.
         * @param previousKey The original key that was bound to the modifier
         *        key.
         */
        public ModifierKeyAction(int key, boolean pressed, Action originalAction, 
                                  Object previousKey) 
        {
            super();
            
            this.key            = key;
            this.pressed        = pressed;
            this.originalAction = originalAction;
            this.previousKey    = previousKey;
        }
        
        
        
        /**
         * Returns the previous <code>Action</code> that was bound to the
         * {@link #getPreviousKey previous key} or <code>null</code> if no
         * previous <code>Action</code> was bound. 
         * 
         * @return The previous <code>Action</code>.
         */
        public Action getPreviousAction() {
            return originalAction;
        }
        
        /**
         * Returns the previous key that was bound to the modifier key or
         * <code>null</code> if no previous key was bound.
         * 
         * @return The previous key.
         */
        public Object getPreviousKey() {
            return previousKey;
        }
        
        /**
         * Performed when the relevant key is pressed or released.
         * <p>
         * This method should not be called directly.
         * </p>
         * 
         * @param e The <code>ActionEvent</code>.  Not used by this method
         *        directly but it is forwarded to the original 
         *        <code>Action</code> if one exists.
         */
        public void actionPerformed(ActionEvent e) {
            if (originalAction != null && !consumeEvents && !inGesture) {
                originalAction.actionPerformed(e);
            }
            
            if (pressed) { 
                currentKeyModifiers.put(key, null);
                
                if (!inGesture && checkForValidMods()) {
                    startGesture(previous);
                }
                
            } else {
                currentKeyModifiers.remove(key);
                
                if (inGesture && !checkForValidMods()) {
                    endGesture(previous);
                }
                
            }
        }
        
    }
    
}
